Skip to content

fix(dash0): stop pre-filling a webmail domain as the first check target - #532

Merged
fclairamb merged 4 commits into
mainfrom
fix/signup-path-defects
Oct 12, 2026
Merged

fclairamb merged 4 commits into
mainfrom
fix/signup-path-defects

Conversation

@fclairamb

Copy link
Copy Markdown
Owner

Three defects on the signup path, found by reading PostHog replays of real signups that ended with zero checks.

1. Fixed: the first check target was the user's mail provider

The zero-check dashboard pre-fills the signup's email domain as the URL to monitor, and skips free webmail. The skip list only held .com names. A recent @gmx.de signup got gmx.de pre-filled, never typed anything, and left without a check. Same for @yahoo.fr, @hotmail.co.uk, @web.de, @orange.fr and so on.

  • Country variants of the big providers are matched by brand: gmx.de, gmx.net, yahoo.co.uk, hotmail.fr, mail.ru. A brand label deeper in the name is not (mail.acme.com is still suggested).
  • National providers are listed: web.de, t-online.de, orange.fr, free.fr, laposte.net, libero.it, btinternet.com, uol.com.br...
  • The same rule backs validateAutoJoinRegex, so an admin can no longer set .+@gmx\.de and auto-admit every signup from that provider. Nothing sets that pattern automatically, so this was defense in depth only.

Files: server/internal/handlers/auth/autojoin_regex.go (source of truth) and web/dash0/src/lib/quick-target-suggestion.ts (mirror), with tests on both sides.

2. Spec: a first-time visitor is told their session expired

specs/todos/2026-10-12-01-first-visit-no-session-expired-banner.md. Opening solidping.io in a fresh browser lands on /d/orgs/default/login?session_expired=true with "Your session has expired". Cause: RootRedirect sends a visitor with no session to /no-org, its requests 401, and handleResponse always calls redirectToExpiredLogin(), which always sets the flag.

3. Already filed: signing up from the demo files a join request to demo

Covered by specs/todos/2026-10-10-02-demo-org-no-membership-request.md, nothing added here.

Checks

  • go test -short ./internal/handlers/auth/ passes
  • golangci-lint run ./...: 0 issues
  • vitest run src/lib/quick-target-suggestion.test.ts: 51 passed
  • eslint clean on the changed files

🤖 Generated with Claude Code

fclairamb and others added 2 commits October 12, 2026 01:24
The zero-check dashboard pre-fills the signup's email domain as the URL to
monitor, skipping free webmail. The skip list only had .com names, so a
@gmx.de, @yahoo.fr or @orange.fr signup was offered its mail provider as its
first check.

Country variants of the big providers (gmx.de, yahoo.co.uk, hotmail.fr,
mail.ru) are now matched by brand, and national providers (web.de,
t-online.de, orange.fr, free.fr, laposte.net...) are listed. The same rule
backs the auto-join pattern check, so an admin can no longer claim
@gmx\.de and auto-admit every signup from that provider.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.76%. Comparing base (44840fd) to head (0e88181).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #532      +/-   ##
==========================================
+ Coverage   82.71%   82.76%   +0.04%     
==========================================
  Files        1175     1175              
  Lines      121451   121460       +9     
==========================================
+ Hits       100463   100525      +62     
+ Misses      20981    20928      -53     
  Partials        7        7              
Files with missing lines Coverage Δ
server/internal/handlers/auth/autojoin_regex.go 97.77% <100.00%> (+0.55%) ⬆️

... and 22 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@fclairamb
fclairamb merged commit b3e6672 into main Oct 12, 2026
26 checks passed
@fclairamb
fclairamb deleted the fix/signup-path-defects branch October 12, 2026 05:35
fclairamb added a commit that referenced this pull request Oct 14, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.38.4](v0.38.3...v0.38.4)
(2026-10-14)


### Features

* **http:** **HTTP results split the response time into DNS, connect,
TLS, server and download time.** The result page shows a Timing card,
and the check page has a Timing chart next to the response time. Each
probe now opens a new connection, so a check that runs more often than
every 90 s now counts connect and TLS time in its response time. The new
metrics are `dns_time_ms`, `connect_time_ms`, `tls_time_ms`,
`ttfb_time_ms`, `download_time_ms` and `redirect_time_ms`.
([9947d0b](9947d0b))
* **http:** **HTTP checks can test response headers with equals,
contains, absent and other operators.** The new `headerAssertions` takes
the same operators as body assertions, plus `exists` and `not_exists`.
Rules like "`X-Powered-By` must not be sent" or "`Cache-Control` must
not contain `no-store`" are now possible. The check form has a new
"Header assertions" section, and `headers_pattern` keeps working.
([9947d0b](9947d0b))
* **http:** **An HTTP check can test where its redirects end up.** Set
`expectedFinalUrl`, and the check goes down when the final URL is
different, for example a login or parking page. The comparison ignores
the case of the scheme and host, default ports and the fragment. The
failure output includes `final_url`.
([9947d0b](9947d0b))
* **dash0:** **An HTTP check can be copied as a cURL command.** "Copy as
cURL" is in the check page menu and on the incident page. The command
has the method, headers, body, redirects, TLS verification, HTTP version
and timeout. Secrets are never included: basic auth becomes
`$SOLIDPING_BASIC_AUTH`, and a comment line says when secret headers
were left out.
([9947d0b](9947d0b))
* **checks:** **Checks can send a new value on every run to get past a
CDN cache.** `${var:timestamp}`, `${var:iso8601}`, `${var:uuid}` and
`${var:random}` work anywhere `${env:...}` does, for example
`?cb=${var:timestamp}`. Within one run, each name keeps the same value.
The URL stored in a result now keeps `${param:...}` references instead
of the secret values.
([9947d0b](9947d0b))
* **icmp:** **An ICMP check can go down on packet loss or a slow round
trip.** Set `max_packet_loss_percent` or `max_avg_rtt_ms`. Before, one
reply out of five kept the check up. The incident gives the reason, for
example `packet loss 40% > 20%`.
([9947d0b](9947d0b))
* **notifications:** **Incident notifications say which regions see the
failure.** Alerts include "Failing from: Europe West, US East (2 of 3
regions)", and resolved alerts include "Failed from: ...". The webhook
payload has a new `incident.regions` with `failingRegions` (slugs),
`regionCount` and `failQuorum`. It is recorded when the incident opens
and updated when it escalates.
([9947d0b](9947d0b))
* **incidents:** **Open incidents that nobody has acknowledged can send
reminders.** Set an interval from 15 min to 24 h on the check
(`reminderIntervalSeconds`) or as an organization default. The check's
channels then get `incident.reminder`: "Still down after 1 h 15 min, not
acknowledged (reminder 3)". Reminders are off by default and stop on
ack, snooze or resolve. Twilio and PagerDuty channels never get them.
([9947d0b](9947d0b))
* **mcp:** **The MCP server lists 23 tools by default instead of 46.**
Tools are grouped in toolsets. `core` and `statuspages` are listed by
default, and `admin` (integrations and maintenance windows) must be
requested. Add `?toolsets=all` to the endpoint URL to list them all, or
use `SP_MCP_TOOLSETS` or `--toolsets` with stdio. The 19 replaced tool
names still work and are listed with `toolsets=legacy`.
([9947d0b](9947d0b))
* **helm:** **The Helm chart can run SolidPing on PostgreSQL through
CloudNativePG.** Set `database.cnpg.enabled=true` to create a CNPG
`Cluster` and read the connection string from its secret. The cluster
survives `helm uninstall`. SQLite stays the default, so existing
installs are not moved on upgrade. The Kubernetes install page now
documents the chart.
([#544](#544))
([30218d5](30218d5))


### Bug Fixes

* **discord:** **Discord buttons work when the bot has no Interactions
Endpoint URL.** Acknowledge, I'm unavailable and Escalate failed with
"The bot did not respond quick enough", and nothing showed in the logs.
Discord sends the button press over the Gateway in that case, and
SolidPing ignored it. It now answers it like the HTTPS endpoint does, so
the endpoint URL is optional when the Gateway is on.
([#557](#557))
([3b5937f](3b5937f))
* **http:** **An HTTP check with no expected status passes on any 2xx,
not only 200.** Healthy endpoints answering 201, 202 or 204 showed as
down. Existing checks change too. To keep the old behaviour, set
`expectedStatusCodes: ["200"]`.
([9947d0b](9947d0b))
* **db:** **A first start that was interrupted no longer leaves a
database that never starts again.** Stopping a container during its
first start could end with `no such column:
organization.default_escalation_policy_uid` on every boot. A failed or
interrupted migration now stays pending and runs again. On SQLite, the
first boot builds the database in a temporary file and renames it when
done, and each upgrade first saves a backup
(`solidping.db.pre-<migration>.bak`) when there is room on disk.
([#549](#549))
([c7479ed](c7479ed))
* **webpush:** **Push notifications keep working after the first
restart.** On a fresh install, the push key served during the first run
was never stored, so the first restart made a new one and every browser
subscribed in between stopped receiving pushes. The key is now stored
before it is used. A browser hit by this can turn push off and on again.
([#539](#539))
([caf64b4](caf64b4))
* **server:** **HEAD requests get the same answer as GET.** Every route
answered `405 Method Not Allowed` to HEAD, so external uptime monitors
that probe with HEAD saw SolidPing status pages as down.
([9947d0b](9947d0b))
* **checks:** **Run now during a run in progress now gives a result.**
The request was dropped and the region reported `running`, so with a 1 h
period the dashboard waited forever. The region now answers `queued` and
runs again as soon as the current run ends. Editing a check while it
runs no longer loses that run's result.
([9947d0b](9947d0b))
* **server:** **A fresh instance idles at about 30 MiB of its own memory
instead of 140.** The seeded admin password was hashed with 64 MiB
argon2id, and that memory stayed resident for minutes. Every login did
the same. Measured on linux/arm64, RssAnon went from 141 to 31 MiB 60 s
after first boot, and from 207 to 26 MiB 10 s after a login. The browser
sample checks are now created disabled, and a Linux host with 2 GiB of
RAM or less and no cgroup limit gets half its RAM as the soft memory cap
(`SP_RUNTIME_AUTO_MEMORY_LIMIT=false` turns this off).
([9947d0b](9947d0b))
* **auth:** **On a host with 2 GiB of memory or less, password hashing
uses the OWASP minimum argon2id profile.** A login now allocates 19 MiB
instead of 64 MiB (`m=19456 t=2 p=1`). This applies only to
single-process installs (`SP_NODE_ROLE=all`) where no argon2 cost is
set. Existing hashes are converted at the next login.
`SP_AUTH_PASSWORD_ARGON2_AUTO_PROFILE=false` keeps 64 MiB.
([9947d0b](9947d0b))
* **onboarding:** **A fresh install seeds 11 sample checks that work,
instead of about 75.** Most samples targeted services nobody runs
locally (databases, message brokers, game servers, test FTP servers), so
about half of them were down and the first dashboard was red. The 11
seeded checks are HTTP, DNS, SSL and TCP checks against public services.
Every sample is still in the create-check picker, and test and demo
modes seed them all.
([#537](#537))
([262c2cc](262c2cc))
* **logs:** **Logs pick a readable format on their own.** The new
default format, `auto`, gives JSON on Kubernetes, colored output in a
terminal or in a container, and plain text otherwise.
`SP_LOG_FORMAT=json`, `text` or `pretty` still wins. The colored format
has a timestamp and keeps attributes in a stable order. `NO_COLOR` and
`FORCE_COLOR` are honored. `LOG_LEVEL` is no longer reset to info once
the configuration loads.
([#536](#536))
([4c13730](4c13730))
* **logs:** **A fresh server prints a few lines at startup and the URL
to open.** It used to print 541 lines in its first minute and never said
where to go. Check runs are DEBUG now. An INFO line is written when a
check goes down, comes back up, warns or goes stale, and names the
check. Workers and sample seeding print one line each.
([#538](#538))
([bbe682a](bbe682a))
* **logs:** **A fresh start no longer warns about normal things.** The
SQL errors on a missing table, the worker hostname, the retention
setting, Sentry and Redis warnings are gone or moved to DEBUG. The
metrics endpoint gets one line instead of two that contradicted each
other. A fresh start now prints one WARN: credentials encryption is
disabled. ([#539](#539))
([caf64b4](caf64b4))
* **auth:** **Signing in on the demo org no longer files a join
request.** A visitor who signed up from the live demo was told "A join
request was sent to its admins", and nobody ever answered it. Sign-ins
on the demo org, and on the `default` org in SaaS mode, now land on the
create-your-org screen. On self-hosted installs, sign-ins on the
`default` org still send join requests to its admins.
([9947d0b](9947d0b))
* **dash0:** **A first-time visitor is no longer told their session
expired.** Opening `/` or `/d/` in a fresh browser showed "Your session
has expired. Please log in again." The visitor now lands on the sign-in
page with no banner. An expired or revoked session still shows it.
([9947d0b](9947d0b))
* **onboarding:** **The first check is no longer pre-filled with your
mail provider.** The empty dashboard suggests your email domain as the
first URL to monitor, and skips free webmail. The skip list only had
`.com` names, so a `gmx.de`, `yahoo.fr` or `orange.fr` address got its
mail provider suggested. Country variants and national providers are now
skipped too. ([#532](#532))
([b3e6672](b3e6672))
* **webhooks:** **Webhook signing secrets work as is with the official
Standard Webhooks libraries.** New secrets are `whsec_` followed by
standard base64 with padding. The dashboard and API show older secrets
in the same form. It is the same key, so receivers that already verify
keep working and nothing needs rotating. A pasted secret that is not
valid base64 now gets a 400 on create and update.
([9947d0b](9947d0b))
* **config:** **`SP_AUDIT_RETENTION_DAYS=0` keeps audit events forever,
as documented.** The value 0 was ignored, and the cleanup still deleted
events older than 365 days. 0 now means "keep forever" in the env
variable, in `config.yml` and in the `audit.retention_days` server
parameter.
([9947d0b](9947d0b))
* **helm:** **Uploaded files survive a pod restart on PostgreSQL.** With
`database.type=postgres`, `/data` was not mounted, so logos, status page
assets and screenshots were lost on every restart. `/data` is now
mounted whatever the database.
([#544](#544))
([30218d5](30218d5))
* **dash0:** **Toasts no longer cover the Run now, Disable and Edit
buttons on a check page.** After you created a check, the "Not on any
status page yet" toast sat over these buttons. It is now a hint inside
the check page, with its "Publish it" action. All toasts now start below
the page header.
([9947d0b](9947d0b))
* **dash0:** **Region events name the region.** On the Events page,
`region.offline` and `region.recovered` had an empty detail. They now
show the region name.
([#552](#552))
([f3ac93e](f3ac93e))
* **dash0:** **Page titles stay readable on a phone.** On narrow
screens, the header buttons squeezed the title to one word per line, for
example on SLOs and Escalation policies. The buttons now wrap below the
title. ([#553](#553))
([01d1c95](01d1c95))
* **dash0:** **The status page editor works on a phone.** Section
headers and resource rows wrap instead of overflowing.
([#554](#554))
([fb53d50](fb53d50))
* **http:** **Forcing HTTP/2 against an HTTP/1.1-only server always
gives the clear error.** About 2% of such cleartext probes reported a
generic connection error instead of "expected HTTP/2, but the server did
not negotiate it".
([#547](#547))
([b74bf7c](b74bf7c))
* **server:** **The platform watchdog no longer flags daily checks as
stale.** A heartbeat check that runs once a day was reported as "silent
for 95h" between the compaction of its last result and its next run. The
watchdog now also reads the check's last result time.
([#542](#542))
([bd4af36](bd4af36))
* **api:** **The number of check types is the same everywhere.** The
docs said 39 in one place and 43 in another. Every page now says 43.
`GET /api/v1/check-types` has a new `excludedFromTally` field, true for
`sleep` and `private-location`. The incidents API docs also list the
parameters the server actually reads (`state`, `checkUid`, `cursor`).
([9947d0b](9947d0b))
* **deps:** update github.com/dop251/goja digest to 0c308c5
([#546](#546))
([cba413a](cba413a))
* **deps:** update github.com/dop251/goja digest to 245c54f
([#551](#551))
([90b9c25](90b9c25))
* **deps:** update github.com/dop251/goja digest to 480b366
([#545](#545))
([b86f196](b86f196))
* **deps:** update go dependencies (non-major)
([#543](#543))
([87eb548](87eb548))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant